<?php

class Ingredient
{
    CONST NAME_MAX_LEN = 10;
    CONST QUANLITY_MAX_LEN = 10;

    public $name;
    public $quanlity;
    
    public function __construct($n='', $q='')
    {
        $this->name=isset($n)?$n:'';
        $this->quanlity=isset($q)?$q:'';
    }
    
    public function validate($charset=null)
    {
        if(!isset($this->name)||!isset($this->quanlity)) return false;
        
        if($charset===null)
            $charset=Yii::app()->charset;

        if(mb_strlen($this->name, $charset)>self::NAME_MAX_LEN || mb_strlen($this->quanlity, $charset)>self::QUANLITY_MAX_LEN)
            return false;

        return true;
    }
}

class Recipe extends CActiveRecord
{
    CONST TITLE_MAX_LEN = 20;
    CONST DESCRIPTION_MAX_LEN = 200;
    CONST SERVINGS_MAX_LEN = 10;
    CONST TIP_MAX_LEN = 100;
    CONST EFFECTIVENESS_MAX_LEN = 50;

    CONST MAX_INGREDIENTS = 100;
    CONST MIN_STEPS = 1;
    CONST MAX_STEPS = 100;
    CONST INIT_STEPS = 4;

    public static $statusEnum=array(
        'draft'=>0,
        'published'=>1,
    );
    
    public static $autoLoadAttributes=array('id','userId','status');

    public $ingredients=array();
    public $steps=array();
    
    private $_image;
    private $_ingredients;
    private $_steps;
    private $_saveAttributes;

    
    /********************************************************************
    *  Below are class functions
    *  WARNING: You should only use these functions to manipulate 
    *           the object instead of AR functions.
    *********************************************************************/
    
    public static function loadObjectById($id,$attributes='*')
    {
        return array_shift(self::loadObjectsByIds(array($id),$attributes));
    }

    public static function loadObjectsByIds($ids,$attributes='*')
    {
        if(is_array($attributes)){
            $attributes=array_unique(array_merge($attributes,self::$autoLoadAttributes));
        }
        $r=array();
        foreach($ids as $id) {
            $r[]=self::loadObject(
                array('condition'=>'id=:id', 'params'=>array(':id'=>$id), 'select'=>$attributes)
            );
        }
        return $r;
    }
    
    public static function loadObjectsByUserId($userId, $options=array())
    {
        if($options instanceof CDbCriteria) {
            $criteria=&$options;            
        }
        else {
            $criteria=new CDbCriteria($options);
        }

        if(is_array($criteria->select)){
            $criteria->select=array_unique(array_merge($criteria->select,self::$autoLoadAttributes));
        }

        return self::loadObjects(array(
            'condition'=>'userId=:userId',
            'params'=>array(':userId'=>$userId),
            'select'=>$criteria->select,
            'offset'=>$criteria->offset,
            'limit'=>$criteria->limit,
            'order'=>'updateTime desc',
        ));
    }
    
    public static function loadPublishedObjectsByUserId($userId, $options=array())
    {
        if($options instanceof CDbCriteria) {
            $criteria=&$options;            
        }
        else {
            $criteria=new CDbCriteria($options);
        }

        if(is_array($criteria->select)){
            $criteria->select=array_unique(array_merge($criteria->select,self::$autoLoadAttributes));
        }

        return self::loadObjects(array(
            'condition'=>'userId=:userId and status=:status',
            'params'=>array(':userId'=>$userId,':status'=>self::$statusEnum['published']),
            'select'=>$criteria->select,
            'offset'=>$criteria->offset,
            'limit'=>$criteria->limit,
            'order'=>'updateTime desc',
        ));
    }    

    public static function countPublishedObjects()
    {
        return self::model()->count('status=:status', array(':status'=>self::$statusEnum['published']));
    }    
    
    public static function loadPublishedObjects($options=array())
    {
        if($options instanceof CDbCriteria) {
            $criteria=&$options;            
        }
        else {
            $criteria=new CDbCriteria($options);
        }

        if(is_array($criteria->select)){
            $criteria->select=array_unique(array_merge($criteria->select,self::$autoLoadAttributes));
        }

        return self::loadObjects(array(
            'condition'=>'status=:status',
            'params'=>array(':status'=>self::$statusEnum['published']),
            'select'=>$criteria->select,
            'offset'=>$criteria->offset,
            'limit'=>$criteria->limit,
            'order'=>'publishTime desc',
        ));
    }    
    
    public static function loadObject($criteria=array())
    {
        $criteria['limit']=1;                                 
        return array_shift(self::loadObjects($criteria));
    }
    
    public static function loadObjects($criteria=array())
    {
        return self::model()->findAll($criteria);
    }
    
    public static function saveObject(&$object, $attributes=null)
    {
        if(is_array($attributes))
            $object->_saveAttributes=array_flip($attributes);
        else
            $object->_saveAttributes=$attributes;

        if($object->getIsNewRecord()){
            $ret=self::createObject($object, $attributes);
        }
        else {
            $ret=$object->save(true, $attributes);
        }

        unset($object->_saveAttributes);
        return $ret;        
    }
            
    public static function updateObjectCountersById($id,$counters)
    {
        return self::updateObjectCounters(
            $counters,
            array('condition'=>'id=:id', 'params'=>array(':id'=>$id))
        );
    }

    public static function updateObjectCounters($counters, $criteria=array())
    {
        return self::model()->updateCounters($counters, $criteria);
    }
    
    public function updateObjectById($id, $attributes)
    {
        return self::updateObjects($attributes, 'id=:id', array(':id'=>$id));
    }

    public function updateObjects($attributes,$condition='',$params=array())
    {
        return self::model()->updateAll($attributes, $condition, $params);
    }
    
    public static function deleteObject(&$object)
    {
        $transaction=Yii::app()->db->beginTransaction();
        try {
            if(!isset($object->status)){
                $recipe=self::loadObjectById($object->id, array('status'));
                $object->status=$recipe->status;
            }
            
            $updateCounters=array('recipeNum'=>-1);
            if($object->status==self::$statusEnum['published']) {
                $updateCounters['publishedRecipeNum']=-1;
            }
            
            if(!$object->delete()) {
                $transaction->rollBack();
                return false;
            }

            Step::deleteObjectsByRecipeId($object->id);
            Kitchen::updateObjectCountersById($object->userId, $updateCounters);

            $transaction->commit();
            return true;
        }
        catch(Exception $e){
            $transaction->rollBack();
            throw $e;
        }
    }
    
    
    /********************************************************************
    *  Below are some logic functions
    *********************************************************************/

    public static function publish(&$object)
    {
        $transaction=Yii::app()->db->beginTransaction();
        try {
            $object->status=self::$statusEnum['published'];
            $object->publishTime=time();
            if(!Recipe::saveObject($object,array('status','publishTime','updateTime'))){
                $transaction->rollBack();
                return false;            
            }

            Kitchen::updateObjectCountersById($object->userId, array('publishedRecipeNum'=>1));
            $transaction->commit();
            return true;
        }
        catch(Exception $e){
            $transaction->rollBack();
            throw $e;
        }
    }
        
    public static function appendStep(&$object, $prevStepId)
    {
        $transaction=Yii::app()->db->beginTransaction();
        try {
            if(!$object->steps){
                $recipe=self::loadObjectById($object->id, array(steps));
                $object->steps=$recipe->steps;
            }

            $prevStepKey = array_search($prevStepId, $object->steps);
            if($prevStepKey===false){
                throw new CException("step {$prevStepId} does not exist in recipe {$object->id}");
            }
            
            $step=new Step;
            $step->recipeId=$object->id;
            $step->userId=$object->userId;
            if(!Step::saveObject($step)){
                throw new CException('init step failed');
            }

            array_splice($object->steps,$prevStepKey+1,0,array($step->id));

            if(!Recipe::saveObject($object,array('steps','updateTime'))){
                $transaction->rollBack();
                return false;            
            }

            $transaction->commit();
            return $step;
        }
        catch(Exception $e){
            $transaction->rollBack();
            throw $e;
        }        
    }            

    public static function deleteStep(&$object, $stepId)
    {
        $transaction=Yii::app()->db->beginTransaction();
        try {
            if(!$object->steps){
                $recipe=self::loadObjectById($object->id, array(steps));
                $object->steps=$recipe->steps;
            }
            
            $stepKey = array_search($stepId, $object->steps);
            if($stepKey===false){
                throw new CException("step {$stepId} does not exist in recipe {$object->id}");
            }
            array_splice($object->steps,$stepKey,1);

            if(!Recipe::saveObject($object,array('steps','updateTime'))){
                $transaction->rollBack();
                return false;            
            }
            
            $step=new Step;
            $step->id=$stepId;
            $step->recipeId=$object->id;
            $step->isNewRecord=false;
            if(!Step::deleteObject($step)){
                $transaction->rollBack();
                return false;                
            }

            $transaction->commit();
            return true;
        }
        catch(Exception $e){
            $transaction->rollBack();
            throw $e;
        }        
    }            

    public static function forwardStep(&$object, $stepId)
    {
        if(!$object->steps){
            $recipe=self::loadObjectById($object->id, array(steps));
            $object->steps=$recipe->steps;
        }
            
        $stepKey = array_search($stepId, $object->steps);
        if($stepKey===false){
            throw new CException("step {$stepId} does not exist in recipe {$object->id}");
        }
        if($stepKey===0){
            return true;
        }        

        $prevStep = $object->steps[$stepKey-1];
        $object->steps[$stepKey-1] = $object->steps[$stepKey];
        $object->steps[$stepKey] = $prevStep;
        return Recipe::saveObject($object, array('steps','updateTime'));
    }            

    public static function backwardStep(&$object, $stepId)
    {
        if(!$object->steps){
            $recipe=self::loadObjectById($object->id, array(steps));
            $object->steps=$recipe->steps;
        }
            
        $stepKey = array_search($stepId, $object->steps);
        if($stepKey===false){
            throw new CException("step {$stepId} does not exist in recipe {$object->id}");
        }
        if($stepKey===count($object->steps)-1){
            return true;
        }        

        $nextStep = $object->steps[$stepKey+1];
        $object->steps[$stepKey+1] = $object->steps[$stepKey];
        $object->steps[$stepKey] = $nextStep;
        return Recipe::saveObject($object,array('steps','updateTime'));
    }            

    protected static function createObject(&$object, $attributes=null)
    {
        $transaction=Yii::app()->db->beginTransaction();
        try {
            for($i=0; $i<self::INIT_STEPS; ++$i){
                $object->steps[$i]=0;
            }
            
            $object->status=self::$statusEnum['draft'];

            if(!$object->save(true, $attributes))
                return false;
            
            for($i=0; $i<self::INIT_STEPS; ++$i){
                $step=new Step;
                $step->recipeId=$object->id;
                $step->userId=$object->userId;
                if(!Step::saveObject($step)){
                    throw new CException('init steps failed');
                }
                $object->steps[$i]=$step->id;
            }
            
            if(!$object->save(true, 'steps'))
                throw new CException('save recipe steps failed');
                
            Kitchen::updateObjectCountersById($object->userId, array('recipeNum'=>1));

            $transaction->commit();
            return true;
        }
        catch(Exception $e){
            $transaction->rollBack();
            $object->steps=array();
            throw $e;
        }
    }
    

    /********************************************************************
    *  Below are object & AR required functions
    *********************************************************************/

    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    
	public function tableName()
	{
		return 'Recipe';
	}

	public function rules()
	{
		return array(
			array('title,userId', 'required'),
            array('title,description,steps,ingredients,userId,userName,id','required',on=>'publish'),
            array('title', 'WStringValidator', 'max'=>self::TITLE_MAX_LEN),
            array('description', 'WStringValidator', 'max'=>self::DESCRIPTION_MAX_LEN),
            array('servings', 'WStringValidator', 'max'=>self::SERVINGS_MAX_LEN),
			array('userId, commentNum, ratingNum, rating, createTime, updateTime', 'numerical', 'integerOnly'=>true),
            array('ingredients', 'validateIngredients'),
            array('steps', 'validateSteps'),
            array('tip', 'WStringValidator', 'max'=>self::TIP_MAX_LEN),
            array('effectiveness', 'WStringValidator', 'max'=>self::EFFECTIVENESS_MAX_LEN),
		);
	}

	public function attributeLabels()
	{
		return array(
			'id' => 'Id',
			'userId' => 'userId',
            'userName' => '创建者',
			'title' => '菜名',
			'description' => '描述',
            'image' => '图片',
			'ingredients' => '配料',
			'steps' => '制法',
			'tip' => '小贴士',
            'effectiveness' => '功效',            
			'commentNum' => 'Comment Num',
			'ratingNum' => 'Rating Num',
			'rating' => '星级',
            'status' => '状态',
			'createTime' => '创建时间',
			'updateTime' => '更新时间',
            'publishTime' => '发布时间',            
		);
	}

    public function validateIngredients($object,$attribute)
    {
        if(!is_array($this->ingredients)){
            $this->addError('ingredients', $this->getAttributeLabel('ingredients').' 错误');
            return false;
        }
        if(sizeof($this->ingredients) > self::MAX_INGREDIENTS ) {
            $this->addError('ingredients', $this->getAttributeLabel('ingredients').' 超过最大值');
            return false;
        }        
        foreach($this->ingredients as $ingredient){
            if(!$ingredient->validate()){
                $this->addError('ingredients', $this->getAttributeLabel('ingredients').' 内容超过最大长度');
                return false;
            }
        }
        if($this->scenario==='publish'){
            if(empty($this->ingredients)) {
                $this->addError('ingredients', $this->getAttributeLabel('ingredients').' 不能为空');
                return false;                
            }
        }
        
        return true;        
    }

    public function validateSteps($object,$attribute)
    {
        if(!is_array($this->steps)){
            $this->addError('steps', $this->getAttributeLabel('steps').' 错误');
            return false;
        }
        if(sizeof($this->steps) < self::MIN_STEPS) {
            $this->addError('steps', $this->getAttributeLabel('steps').' 不能少于'.self::MIN_STEPS);
            return false;        
        }
        if(sizeof($this->steps) > self::MAX_STEPS) {
            $this->addError('steps', $this->getAttributeLabel('steps').' 不能多于'.self::MAX_STEPS);
            return false;
        }
        
        return true;
    }    
    
    public function isPublished ()
    {
        return $this->status == self::$statusEnum['published'];
    }
    
    protected function isSaveAttribute($name)
    {
        if(is_array($this->_saveAttributes))
            return array_key_exists($name,$this->_saveAttributes);
        return true;
    }
    
    protected function beforeValidate()
    {
        if($this->isNewRecord)
            $this->createTime=$this->updateTime=time();
        else
            $this->updateTime=time();
        return true;
    }

    protected function beforeSave()
    {
        if($this->isSaveAttribute('image')){
            $this->_image = $this->image;
            $this->image = json_encode($this->image);
        }
        if($this->isSaveAttribute('ingredients')){
            $this->_ingredients = $this->ingredients;
            $this->ingredients = json_encode($this->ingredients);
        }
        if($this->isSaveAttribute('steps')){
            $this->_steps = $this->steps;
            $this->steps = json_encode($this->steps);
        }
        return true;
    }

    protected function afterSave()
    {
        if($this->isSaveAttribute('image')){
            $this->image = $this->_image;
            $this->_image = null;
        }
        if($this->isSaveAttribute('ingredients')){
            $this->ingredients = $this->_ingredients;
            $this->_ingredients = null;
        }
        if($this->isSaveAttribute('steps')){
            $this->steps = $this->_steps;
            $this->_steps = null;
        }
    }

    protected function afterFind()
    {
        if(is_string($this->image)){
            $this->image = json_decode($this->image,true);
        }
        if(is_string($this->ingredients)){
            $ingredients = json_decode($this->ingredients);
            $this->ingredients=array();        
            foreach($ingredients as $ingredient) {
                $this->ingredients[]=new Ingredient($ingredient->name, $ingredient->quanlity);
            }
        }
        if(is_string($this->steps)){
            $this->steps = json_decode($this->steps);
        }
    }
}